package com.demo.zk.分布式锁;

import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;

/**
 * 分布式锁：
 * 主要方法：
 * 类似mq操作，每一个连接都创建一个节点，然后节点相互监听前面一个，等前面执行完毕轮到自己。其实就是公平锁。
 * 加锁操作：创建一个节点，并且设置监听前面一个节点，发生删除事件后轮到自己。
 * 解锁操作：删除节点
 */
public class DistributedLock {

    // zookeeper server列表
    private final String connectString = "hadoop102:2181";
    /**
     * 超时时间
     */
    private final int sessionTimeout = 2000;
    private final ZooKeeper zk;
    /**
     * 锁的根节点名称
     */
    private final String rootNode = "locks";

    private final String subNode = "seq-";

    //ZooKeeper连接
    private final CountDownLatch connectLatch = new CountDownLatch(1);
    //ZooKeeper节点等待
    private final CountDownLatch waitLatch = new CountDownLatch(1);

    // 当前client等待的子节点
    private String waitPath;
    // 当前client创建的子节点
    private String currentNode;

    /**
     * 和zk服务建立连接，并创建根节点
     */
    public DistributedLock() throws IOException, InterruptedException, KeeperException {

        // 1.创建客户端
        zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
            /**
             * 创建监听方法
             * 里面主要涉及监听的事件
             * 连接客户端成功事件
             * 节点删除事件
             * 是自己监听路径节点变化事件
             */
            @Override
            public void process(WatchedEvent event) {
                // 连接建立时, 打开latch, 唤醒wait在该latch上的线程
                // 如果连接zk成功
                if (event.getState() == Event.KeeperState.SyncConnected) {
                    connectLatch.countDown();
                }
                // 发生了waitPath的删除事件
                // 如果发生节点删除，而且监听的路径等于自己监听的节点
                if (event.getType() == Event.EventType.NodeDeleted && event.getPath().equals(waitPath)) {
                    waitLatch.countDown();
                }
            }
        });

        // 2.等待zk连接建立成功
        connectLatch.await();

        // 3.获取根节点状态"/locks"是否存在
        Stat stat = zk.exists("/" + rootNode, false);

        // 4.如果根节点不存在，则创建根节点，根节点类型为永久节点
        if (stat == null) {
            System.out.println("根节点不存在");
            // 创建一个节点
            zk.create("/" + rootNode, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }

    }

    /**
     * 加锁
     */
    public void zkLock() {
        try {
            // 1.在根节点下创建临时顺序节点，返回值为创建的节点路径
            currentNode = zk.create("/" + rootNode + "/" + subNode, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
            // 2.wait一小会, 让结果更清晰一些
            Thread.sleep(10);
            // 注意, 没有必要监听"/locks"的子节点的变化情况
            List<String> childrenNodes = zk.getChildren("/" + rootNode, false);
            // 列表中只有一个子节点, 那肯定就是currentNode , 说明client获得锁
            if (childrenNodes.size() == 1) {
                return;
            } else {
                // 对根节点下的所有临时顺序节点进行从小到大排序
                Collections.sort(childrenNodes);
                // 当前节点名称 seq-000
                String thisNode = currentNode.substring(("/" + rootNode + "/").length());
                //获取当前节点的位置
                int index = childrenNodes.indexOf(thisNode);
                if (index == -1) {
                    System.out.println("数据异常");
                } else if (index == 0) {
                    // 说明自己到index为0，到自己了
                    // index == 0, 说明thisNode在列表中最小, 当前client获得锁
                    return;
                } else {
                    // 获得排名比currentNode 前1位的节点
                    this.waitPath = "/" + rootNode + "/" + childrenNodes.get(index - 1);
                    // 在waitPath上注册监听器, 当waitPath被删除时,zookeeper会回调监听器的process方法
                    // 监听某一个值：节点值    是否监听    状态
                    zk.getData(waitPath, true, null);
                    // 进入等待锁状态
                    waitLatch.await();
                    return;
                }
            }
        } catch (KeeperException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 解锁
     */
    public void zkUnlock() {
        try {
            zk.delete(this.currentNode, -1);
        } catch (InterruptedException | KeeperException e) {
            e.printStackTrace();
        }
    }
}
